[csapp] Lab3 Attack Lab
Lab3 Attack Lab
1. Introduction
此实验用来熟悉,理解程序运行的时候的 stack 模型。更进一步理解 stack 上可能发生的错误,对常见的攻击手段有深入理解。
2. ctarget(代码注入)
对于没有进行如下限制的程序可以使用此方法来进行攻击
- 申请的stack空间不是随机的,是固定的。
- 使用了不安全的写入方法,或者使用了低级的io,且没有人为的判断缓冲溢出的程序
- 没有限制内存中的某些stack区域中的指令不能被执行。
2.1 Level 1
要求对 buf 的缓冲区进行溢出,进而让 test 函数不会正常返回,而是运行到 touch1 函数。显然我们需要把 test 栈顶的 ret 指针指向的位置进行覆盖,来指向 touch1 函数。通过 objdump -d
指令,得到 ctarget 的汇编代码,找到 touch1 的位置在 00000000004017c0 <touch1>
。在 lab 给出的 writeup 中,我们知道 test 函数 c code 如下:
1 | 1 void test() |
其调用了 getbuf() 函数, 查看该函数的汇编代码如下:
1 | 00000000004017a8 <getbuf>: |
可以得知,该函数在 stack 中申请了 40 bytes,所以我们需要污染41→48 bytes。显然,我们可以得到最简单的注入编码如下:
1 | 00 00 00 00 00 00 00 00 |
可以画出这个攻击的最终内存形态如下:
Figure1: referenced from [1]
2.2 Level 2
这个题目要求 test 函数会执行 touch2 函数, 而 touch2 函数需要一个存储了 cookie 无符号整型的 %rdi,所以我们需要首先把 %rdi 存储这个 cookie 值, 然后再跳转到 touch2 函数。
需要使用到 movq $(cookie), %rdi
的指令,这里 cookie 由下载来的作业中的 cookie 来定。
所以首先写一个包含这个指令的 x.s
文件, 然后使用 gcc -c x.s
来得到 object 文件,然后使用 objdump -d
来得到最终的指令为 48 c7 c7 fa 97 69 59
,
所以基本的思路是这样的:
movq $cookie, %rdi
pushq (address of touch2)
ret
Aside
这里 ret 和 pushq 的作用似乎非常让人疑惑,为什么 ret 会回到 pushq 的地址?这里的 ret 使用的是近返回,近返回的方法是把栈顶之值弹出到指令指针寄存器IP中,然后从这个弹出的指针处进行执行。
Figurew: Referenced from [2]
看图,这是两个不同的指令,编码都不一样。ret表示近返回,retf表示远返回。
所以使用 gcc -c
指令编码如下的程序
1 | movq $cookie, %rdi |
这里就需要找到 getbuf
的栈顶在哪里,我们需要把 test 函数的 ret 函数的 ret address 换成 getbuf 的 栈顶。使用 gdb 调试, 把断点打在 getbuf 初始化完 %rsp 的位置。然后 run -q
运行,并使用 info r rsp
得到寄存器目前存储的地址信息。可以得到如下的答案。
1 | 48 c7 c7 fa 97 b9 59 68 |
可以画出这个攻击的最终内存形态如下:
Figure3: Referenced from [1]
2.3 Level 3
这个题目要求 test 函数会执行 touch3 函数, 而 touch3 函数需要一个存储了 cookie 字符串的 %rdi,所以我们需要首先把 %rdi 存储这个 cookie 字符串的指针值, 然后再跳转到 touch3 函数。
所以这里的思路就是需要把 字符串首先存储到一个位置, 然后把这个位置的首地址传给 %rdi, 然后进入到 touch3 函数执行。
!但是这里 writeup 让我们注意的是当调用hexmatch
和strncmp
时,他们会把数据压入到栈中,有可能会覆盖getbuf
栈帧的数据,所以传进去字符串的位置必须小心谨慎。hexmatch 开辟了110字节的栈帧,strncmp 也会开辟空间,但是就代码来看,*s存放的地址是随机的,如果我们将数据放在 getbuf 的栈空间里面,很有可能就被这两个函数覆盖了。所以在这里的思路是,把字符串传入到 caller→test 的stack 区域中,这样字符串可以避免被覆盖。
!对于 ascii 码, 我们可以使用 man ascii 来查看对应 ascii 码的 hex 形式。
!字符串 必须以 \0
结尾。
那么对应的思路如下:
- 把 字符串 传入到 test 栈区中。
- 把 字符串的指针 传给 %rdi
- pushq touch3 地址
- ret
至于怎么取到字符串的指针,怎么得到这些指令的编码,与 section 2.2 中一致。
1 | 48 c7 c7 a8 dc 61 55 68 |
可以画出这个攻击的最终内存形态如下:
Figure4: Referenced from [1]
3. rtarget(Return-Oriented Programming)
在 section 2 ctarget 中三条都被启用的情况下,只能使用这个方法。
3.0 review
在栈使用了 randomization 来初始化 stack frame 大小;限制了 memory 中 stack 某些区块的代码执行权限的情况下,需要使用 Return-Oriented Programming 的方法,这个方法的基本想法就是使用已经存在的指令来作为自己的指令执行,从而绕开 stack 区域中的限制。一般而言,可以使用的指令后面需要跟有一个 ret 来返回,通过一系列的指令 [op, ret]->[op, ret]->...
可以形成一个 list, 这个形似单链表的结构就是我们的指令的组成。
比如下面的这个具体的例子:
有如下的 c code
1 | void setval_210(unsigned *p){ |
得到 assembly code:
1 | 0000000000400f15 <setval_210>: |
上面这个 assembly code 就可以作为我们的 list 中的一个 node。 比如 48 89 c7
就是 movl %rax, %rdi
的一个指令代码,并且已经存在于 可执行代码块中,所以我们可以让 pc(program counter) 指向这个位置 0x400f15 + 4
来指向这个 movl 指令,在执行完以后需要立即 ret,然后执行到下一个 node。
3.1 Level 2
这个题目让我们做的和上一个 ctarget 中的 level 2 一致。但是我们需要使用 gadget list 来组成所有需要的指令。根据之前的思路,我们是直接把 cookie 存储到 %rdi,需要使用到 movq 和 pop 指令。根据 writeup 的提示,这是可以被使用的 gadget node:
1 | 0000000000000000 <start_farm>: |
而我们需要的代码是:
1 | popq %rax |
popq %rax
的指令字节为:58
,所以我们找到了如下函数:
1 | 00000000004019a7 <addval_219>: |
从中我们可以得出popq %rax
指令的地址为:0x4019ab
movq %rax, %rdi
的指令字节为:48 89 c7
,所以我们找到了如下函数:
1 | 00000000004019a0 <addval_273>: |
从中我们可以得出movq %rax, %rdi
指令的地址为:0x4019a2
最终可以得出需要的指令为
1 | 00 00 00 00 00 00 00 00 |
3.2 Level 3
这个题目让我们做的和上一个 ctarget 中的 level 3 一致。但是我们需要使用 gadget list 来组成所有需要的指令。 因为每次栈的位置是随机的,所以无法直接用地址来索引字符串的起始地址,只能用栈顶地址 + 偏移量来索引字符串的起始地址。对于获得偏移量的操作,leaq(, , ),%rax 这个操作是 gcc 经常用来获得偏移量的一个指令。
我们可以得到下面的方法:
(1)首先获取到%rsp
的地址,并且传送到%rdi
(2)其二获取到字符串的偏移量值,并且传送到%rsi
(3)lea (%rdi,%rsi,1),%rax
, 将字符串的首地址传送到%rax
, 再传送到%rdi
(4)调用touch3
函数
具体的 geaget 从frame.c 函数的 dump 文件中找到就行
最终的答案是:
1 | 00 00 00 00 00 00 00 00 |
Reference
[csapp] Lab3 Attack Lab
https://chenghuawang.github.io/2022/02/11/[csapp] Lab3 Attack Lab/